// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <linux/device.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/pm_wakeup.h>
#include <linux/syscore_ops.h>
#include <mach/debug.h>
#ifdef CONFIG_DRIVERS_HDF_INPUT
#include "hdf_hid_adapter.h"
#endif

#include "lombo_pmc.h"

#define TIMEOUT_COUNT	20 /* 20ms */

struct lombo_pmc *l_pmc;

#ifdef CONFIG_DRIVERS_HDF_INPUT
static InputDevice *g_hidInputDev = NULL;

static InputDevice *HidRegisterHdfKeyPadDev(void)
{
    InputDevice *inputDev = NULL;
    HidInfo hidInfo;

    hidInfo.devType = INDEV_TYPE_KEYBOARD;
    hidInfo.eventType[0] = SET_BIT(EV_KEY);
    hidInfo.keyCode[3L] = SET_BIT(KEY_POWER);
    hidInfo.devName = "hdi-key";
    inputDev = HidRegisterHdfInputDev(&hidInfo);

    return inputDev;
}
#endif

BLOCKING_NOTIFIER_HEAD(charge_notifier_list);

int register_charge_notifier(struct notifier_block *nb)
{
	return blocking_notifier_chain_register(&charge_notifier_list, nb);
}
EXPORT_SYMBOL(register_charge_notifier);

void lombo_set_sys_stat(void)
{
	csp_pmc_set_sys_stat(l_pmc->base);
}

u32 lombo_get_usb_connected(void)
{
	return csp_pmc_get_con_stat(l_pmc->base);
}

/* @return 0 is RCOSC, 1 is LFEOSC */
u32 lombo_get_clk_src_stat(void)
{
	return csp_pmc_get_clk_src_stat(l_pmc->base);
}

void pmc_config_fanout_func(u32 mode)
{
	csp_pmc_config_fanout_func(l_pmc->base, mode);
}
EXPORT_SYMBOL_GPL(pmc_config_fanout_func);

void pmc_config_output_func(u32 value)
{
	csp_pmc_set_fanout_output(l_pmc->base, OUTPUT, value);
}
EXPORT_SYMBOL_GPL(pmc_config_output_func);

static void lombo_key_press_handler(struct input_dev *idev)
{
    input_report_key(idev, KEY_POWER, 1);
    input_sync(idev);

#ifdef CONFIG_DRIVERS_HDF_INPUT
    InputManager *inputManager = GetInputManager();
    if ((g_hidInputDev == NULL) || (inputManager == NULL)) {
        return;
    }

    pr_warn("%s:%d, key press\n", __func__, __LINE__);

    HidReportEvent(g_hidInputDev, EV_KEY, KEY_POWER, 1);
    HidReportEvent(g_hidInputDev, EV_SYN, SYN_REPORT, 0);
#endif
}

static void lombo_key_short_handler(struct input_dev *idev)
{
    input_report_key(idev, KEY_POWER, 1);
    input_sync(idev);

#ifdef CONFIG_DRIVERS_HDF_INPUT
    InputManager *inputManager = GetInputManager();
    if ((g_hidInputDev == NULL) || (inputManager == NULL)) {
        return;
    }

    pr_warn("%s:%d, key short\n", __func__, __LINE__);

    HidReportEvent(g_hidInputDev, EV_KEY, KEY_POWER, 1);
    HidReportEvent(g_hidInputDev, EV_SYN, SYN_REPORT, 0);
#endif
}

static void lombo_key_long_handler(struct input_dev *idev)
{
    input_report_key(idev, KEY_POWER, 1);
    input_sync(idev);

#ifdef CONFIG_DRIVERS_HDF_INPUT
    InputManager *inputManager = GetInputManager();
    if ((g_hidInputDev == NULL) || (inputManager == NULL)) {
        return;
    }

    pr_warn("%s:%d, key long\n", __func__, __LINE__);

    HidReportEvent(g_hidInputDev, EV_KEY, KEY_POWER, 1);
    HidReportEvent(g_hidInputDev, EV_SYN, SYN_REPORT, 0);
#endif
}

static void lombo_key_release_handler(struct input_dev *idev)
{
    input_report_key(idev, KEY_POWER, 0);
    input_sync(idev);

#ifdef CONFIG_DRIVERS_HDF_INPUT
    InputManager *inputManager = GetInputManager();
    if ((g_hidInputDev == NULL) || (inputManager == NULL)) {
        return;
    }

    pr_warn("%s:%d, key release\n", __func__, __LINE__);

    HidReportEvent(g_hidInputDev, EV_KEY, KEY_POWER, 0);
    HidReportEvent(g_hidInputDev, EV_SYN, SYN_REPORT, 0);
#endif
}

static void lombo_ld_handler(struct input_dev *idev)
{
	u32 clk_src_stat = 0;

	/* disabled LD_INT */
	csp_pmc_ld_int_en(l_pmc->base, 0);
	PRT_WARN("LEFOSC is not detected, the clk src switch to RCOSC!\n");
	clk_src_stat = csp_pmc_get_clk_src_stat(l_pmc->base);
	if (clk_src_stat == 0)
		csp_pmc_set_clk_src(l_pmc->base, CLK_SRC_RCOSC);
}

static void lombo_pmc_work(struct work_struct *work)
{
	u32 record_pend = 0, ld_record_pend = 0;
	unsigned long flags = 0;
	struct lombo_pmc *pmc = container_of(work, struct lombo_pmc, pmc_work);

	usleep_range(300, 500);
	csp_pmc_set_irq_en(pmc->base, pmc->int_en);
	csp_pmc_ld_int_en(pmc->base, 1);
#if defined(CONFIG_ARCH_LOMBO_N7V7)
	csp_pmc_rtc_npd_int_en(pmc->base, 1);
#endif

	spin_lock_irqsave(&pmc->lock, flags);
	record_pend = pmc->irq_pending;
	pmc->irq_pending = 0x0;
	ld_record_pend = pmc->ld_irq_pending;
	pmc->ld_irq_pending = 0x0;
	spin_unlock_irqrestore(&pmc->lock, flags);

	if (record_pend & PWR_KEY_PRESS_IRQ) {
		pm_stay_awake(pmc->idev->dev.parent);
		lombo_key_press_handler(pmc->idev);
	}

	if (record_pend & PWR_KEY_SHORT_IRQ) {
		pm_stay_awake(pmc->idev->dev.parent);
		lombo_key_short_handler(pmc->idev);
	}

	if (record_pend & PWR_KEY_LONG_IRQ) {
		pm_stay_awake(pmc->idev->dev.parent);
		lombo_key_long_handler(pmc->idev);
	}

	if (record_pend & PWR_KEY_RELEASE_IRQ) {
		lombo_key_release_handler(pmc->idev);
		pm_relax(pmc->idev->dev.parent);
	}

	if (ld_record_pend & PMC_LD_PEND_IRQ)
		lombo_ld_handler(pmc->idev);

	if (record_pend & PWR_WAKE0_CON_IRQ)
		blocking_notifier_call_chain(&charge_notifier_list, 1, NULL);

	if (record_pend & PWR_WAKE0_DISCON_IRQ)
		blocking_notifier_call_chain(&charge_notifier_list, 0, NULL);
}

static irqreturn_t lombo_pmc_irq_handle(int irq, void *id)
{
	struct lombo_pmc *pmc = (struct lombo_pmc *)id;
	unsigned long flags = 0;
	u32 pending = 0;

#ifdef CONFIG_DEBUG_SPINLOCK
	if (pmc->lock.rlock.magic != SPINLOCK_MAGIC)
		return IRQ_HANDLED;
#endif

	pending = csp_pmc_get_irq_pending(pmc->base);
	pmc->ld_irq_pending = csp_pmc_get_ld_pending(pmc->base);
	spin_lock_irqsave(&pmc->lock, flags);
	csp_pmc_get_pwrkey_mask(pending, &pmc->irq_pending);
	spin_unlock_irqrestore(&pmc->lock, flags);

	pmc->int_en = csp_pmc_get_irq_en(pmc->base);
	csp_pmc_disable_irq_en(pmc->base);
	csp_pmc_ld_int_en(pmc->base, 0);
#if defined(CONFIG_ARCH_LOMBO_N7V7)
	csp_pmc_rtc_npd_int_en(pmc->base, 0);
#endif

	csp_pmc_clear_irq_pending(pmc->base, pending);
	csp_pmc_ld_int_clr(pmc->base, pmc->ld_irq_pending);

	schedule_work(&pmc->pmc_work);

	return IRQ_HANDLED;
}

void lombo_pmc_init(struct lombo_pmc *pmc, int en)
{
	u32 irq_source, fb_en_source, pending;

	irq_source =  IRQ_SOURCRE;
	fb_en_source = FB_EN_SOURCE;

	if (en)
		fb_en_source |= pmc->sys_resetn;

	/* Init config and interrupt*/
	if (en) {
		csp_pmc_pm_fb_en(pmc->base, fb_en_source);
		csp_pmc_enable_irq_by_source(pmc->base, irq_source);
	} else {
		csp_pmc_pm_fb_disabled(pmc->base, fb_en_source);
		csp_pmc_disable_irq_by_source(pmc->base, irq_source);
	}

	pending = csp_pmc_get_irq_pending(pmc->base);
	csp_pmc_clear_irq_pending(pmc->base, pending);
	csp_pmc_set_pd_mask_en(pmc->base, 1);

	if (pmc->key_slong_mode != EINVAL)
		csp_pmc_set_pwrkey_slong_mode(pmc->base, pmc->key_slong_mode);
}

void lombo_poweroff(void)
{
	csp_pmc_pm_fb_disabled(l_pmc->base, SYS_RESETN);
	csp_pmc_pe_disable(l_pmc->base);
}

static bool rtc_clk_is_available(void)
{
	struct device_node *pnode = NULL;
	bool res = false;

	pnode = of_find_node_by_name(NULL, "rtc");
	if (!pnode)
		return res;
	if (of_device_is_available(pnode))
		res = true;

	return res;
}

static void lombo_pmc_clk_src(struct lombo_pmc *pmc)
{
#ifndef FPGA
	int timeout = 0;

	csp_pmc_ld_en(pmc->base, 1);
	pmc->ld_irq_pending = csp_pmc_get_ld_pending(pmc->base);
	if (pmc->ld_irq_pending)
		csp_pmc_ld_int_clr(pmc->base, pmc->ld_irq_pending);

	/* enabled LD_INT */
	csp_pmc_ld_int_en(pmc->base, 1);

	/* if rtc is alive */
	if (rtc_clk_is_available() && !(pmc->ld_irq_pending)) {
		/* switch clk src to LFEOSC */
		csp_pmc_set_clk_src(pmc->base, CLK_SRC_LFEOSC);
		/* wait for switch clk success */
		while (csp_pmc_get_clk_src_stat(pmc->base) != CLK_SRC_LFEOSC &&
						(timeout < TIMEOUT_COUNT)) {
			PRT_DBG("status: 0x%x ",
					csp_pmc_get_clk_src_stat(pmc->base));
			mdelay(1);
			timeout++;
		}
		if (timeout >= TIMEOUT_COUNT)
			PRT_ERR("time out, LEFOSC is not detected!\n");
	}
#endif
}

static int lombo_pmc_probe(struct platform_device *pdev)
{
	int irq, ret, pending;
	struct resource *iores;
	struct lombo_pmc *pmc;
	int sys_resetn = 0;

	pmc = kzalloc(sizeof(struct lombo_pmc), GFP_KERNEL);
	if (pmc == NULL)
		return -EINVAL;

	iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	pmc->base = ioremap(iores->start, resource_size(iores));
	if (pmc->base == NULL) {
		pr_err("%s:%d, ioremap failed\n", __func__, __LINE__);
		goto err_map;
	}

	csp_pmc_disable_irq_en(pmc->base);
	pending = csp_pmc_get_irq_pending(pmc->base);
	pending &= ~(PWR_WAKE0_CON_IRQ | PWR_WAKE0_DISCON_IRQ);
	csp_pmc_clear_irq_pending(pmc->base, pending);

	pmc->ld_irq_pending = csp_pmc_get_ld_pending(pmc->base);
	if (pmc->ld_irq_pending)
		csp_pmc_ld_int_clr(pmc->base, pmc->ld_irq_pending);

	irq = platform_get_irq(pdev, 0);
	ret = request_irq(irq, lombo_pmc_irq_handle, IRQ_FLAGS,
			"lombo-pmc", (void *)pmc);
	if (ret) {
		pr_err("%s:%d, request irq: %d failed\n",
			__func__, __LINE__, irq);
		goto err_irq;
	}
	pmc->irq = irq;

	/* register input device */
	pmc->idev = input_allocate_device();
	if (pmc->idev == NULL) {
		pr_err("%s:%d, allocate input device failed\n",
			__func__,
			__LINE__);
		goto err_alloc;
	}

	device_init_wakeup(&pdev->dev, 1);

	pmc->idev->name = "lombo-pmc";
	pmc->idev->dev.parent = &pdev->dev;
	set_bit(EV_KEY, pmc->idev->evbit);
	set_bit(KEY_POWER, pmc->idev->keybit);

	ret = input_register_device(pmc->idev);
	if (ret) {
		pr_err("%s:%d, register input device failed\n",
			__func__, __LINE__);
		goto err_inp;
	}

	spin_lock_init(&pmc->lock);

	INIT_WORK(&pmc->pmc_work, lombo_pmc_work);

	platform_set_drvdata(pdev, (void *)pmc);

	of_property_read_u32(pdev->dev.of_node, "sys_resetn", &sys_resetn);
	if (sys_resetn == 1)
		pmc->sys_resetn |= SYS_RESETN;
	else
		pmc->sys_resetn &= ~SYS_RESETN;

	ret = of_property_read_u32(pdev->dev.of_node, "key_slong_mode",
				&pmc->key_slong_mode);
	if (((pmc->key_slong_mode != SLONG_REBOOT_MODE) &&
		(pmc->key_slong_mode != SLONG_POWER_DOWN_MODE)) || ret) {
		pr_warn("%s:%d, not config power_key slong mode, use default\n",
			__func__, __LINE__);
		pmc->key_slong_mode = EINVAL;
	}

	l_pmc = pmc;
	lombo_pmc_clk_src(pmc);
	lombo_pmc_init(pmc, 1);
	pm_power_off = lombo_poweroff;
#ifdef CONFIG_LOMBO_PMC_FANOUT_ENABLE
	pmc_config_fanout_func(FANOUT);
	dev_info(&pdev->dev, "lombo_pmc, lfeosc fanout enable\n");
#endif
	dev_info(&pdev->dev, "lombo pmc init success!");

#ifdef CONFIG_DRIVERS_HDF_INPUT
    int i = 0;
    HidInfo *info = (HidInfo *)kmalloc(sizeof(HidInfo), GFP_KERNEL);
    if (info == NULL) {
        printk("%s: malloc failed\n", __func__);
        return -EINVAL;
    }

    info->devName = pmc->idev->name;
    memcpy(info->devProp, pmc->idev->propbit, sizeof(unsigned long) * BITS_TO_LONGS(INPUT_PROP_CNT));
    memcpy(info->eventType, pmc->idev->evbit, sizeof(unsigned long) * BITS_TO_LONGS(EV_CNT));
    memcpy(info->keyCode, pmc->idev->keybit, sizeof(unsigned long) * BITS_TO_LONGS(KEY_CNT));
    memcpy(info->relCode, pmc->idev->relbit, sizeof(unsigned long) * BITS_TO_LONGS(REL_CNT));
    memcpy(info->absCode, pmc->idev->absbit, sizeof(unsigned long) * BITS_TO_LONGS(ABS_CNT));
    memcpy(info->miscCode, pmc->idev->mscbit, sizeof(unsigned long) * BITS_TO_LONGS(MSC_CNT));
    memcpy(info->ledCode, pmc->idev->ledbit, sizeof(unsigned long) * BITS_TO_LONGS(LED_CNT));
    memcpy(info->soundCode, pmc->idev->sndbit, sizeof(unsigned long) * BITS_TO_LONGS(SND_CNT));
    memcpy(info->forceCode, pmc->idev->ffbit, sizeof(unsigned long) * BITS_TO_LONGS(FF_CNT));
    memcpy(info->switchCode, pmc->idev->swbit, sizeof(unsigned long) * BITS_TO_LONGS(SW_CNT));
    for (i = 0; i < BITS_TO_LONGS(ABS_CNT); i++) {
        if (pmc->idev->absbit[i] != 0) {
            memcpy(info->axisInfo, pmc->idev->absinfo, sizeof(struct input_absinfo) * ABS_CNT);
            break;
        }
    }

    info->bustype = pmc->idev->id.bustype;
    info->vendor = pmc->idev->id.vendor;
    info->product = pmc->idev->id.product;
    info->version = pmc->idev->id.version;
    SendInfoToHdf(info);
    kfree(info);
    info = NULL;

    g_hidInputDev = HidRegisterHdfKeyPadDev();
    if (g_hidInputDev == NULL) {
        pr_err("HidRegisterHdfKeyPadDev error\n");
    }
#endif

	return 0;

err_inp:
	input_free_device(pmc->idev);
err_alloc:
	free_irq(irq, (void *)pmc);
err_irq:
	iounmap(pmc->base);
err_map:
	kfree(pmc);

	return -EINVAL;
}

static int lombo_pmc_remove(struct platform_device *pdev)
{
	struct lombo_pmc *pmc = platform_get_drvdata(pdev);

	cancel_work_sync(&pmc->pmc_work);
	lombo_pmc_init(pmc, 0);
	input_free_device(pmc->idev);
	free_irq(pmc->irq, (void *)pmc);
	iounmap(pmc->base);
	kfree(pmc);

	return 0;
}

static void lombo_pmc_shutdown(struct platform_device *op)
{
	struct lombo_pmc *pmc = platform_get_drvdata(op);

	cancel_work_sync(&pmc->pmc_work);
	lombo_pmc_init(pmc, 0);
	input_free_device(pmc->idev);
	free_irq(pmc->irq, (void *)pmc);
}

static int lombo_pmc_suspend(void)
{
	csp_pmc_set_pd_mask_en(l_pmc->base, 0);

	if (l_pmc->sys_resetn || csp_pmc_get_pm_sys_resetn_status(l_pmc->base))
		csp_pmc_pm_fb_disabled(l_pmc->base, SYS_RESETN);

	return 0;
}

static void lombo_pmc_resume(void)
{
	csp_pmc_set_pd_mask_en(l_pmc->base, 1);

	if (l_pmc->sys_resetn || csp_pmc_get_pm_sys_resetn_status(l_pmc->base))
		csp_pmc_pm_fb_en(l_pmc->base, l_pmc->sys_resetn);
}

static const struct of_device_id lombo_pmc_dt_match[] = {
	{ .compatible = "lombo,n5v1-pmc", },
	{ .compatible = "lombo,n7v1-pmc", },
	{ .compatible = "lombo,n7v3-pmc", },
	{ .compatible = "lombo,n7v5-pmc", },
	{ .compatible = "lombo,n9v1-pmc", },
	{ .compatible = "lombo,n9v3-pmc", },
	{},
};

static struct platform_driver lombo_pmc_driver = {
	.probe  = lombo_pmc_probe,
	.remove = lombo_pmc_remove,
	.shutdown = lombo_pmc_shutdown,
	.driver = {
		.name = "lombo-pmc",
		.of_match_table = lombo_pmc_dt_match,
	},
};

struct syscore_ops lombo_pmc_syscore_ops = {
	.suspend = lombo_pmc_suspend,
	.resume = lombo_pmc_resume,
};

static int __init lombo_pmc_drv_register(void)
{
	register_syscore_ops(&lombo_pmc_syscore_ops);
	return platform_driver_register(&lombo_pmc_driver);
}
module_init(lombo_pmc_drv_register);

static void __exit lombo_pmc_drv_unregister(void)
{
	platform_driver_unregister(&lombo_pmc_driver);
}
module_exit(lombo_pmc_drv_unregister);

MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_DESCRIPTION("Driver for LomboTech Power manager controller");
MODULE_LICENSE("GPL");
